package util

import (
	"context"
	"crypto/md5"
	"crypto/rc4"
	"encoding/base64"
	"fmt"
	baseStore "gitee.com/zaiqiang231/go-base-app/base_app/store"
	"gitee.com/zaiqiang231/go-base-session/app/config"
	"github.com/go-redis/redis/v8"
	"github.com/pkg/errors"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

const (
	SID_VERSION = "80"
	RC4_PASS    = "Ziq007"
	KEY_SID_PRE = "SSID:"
	LETTERS     = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
	TIME_FMT    = "2008-01-01 15:00:00"
)

type ClientType int32

const (
	CLIENT_APP ClientType = iota
	CLIENT_WEB
)

func CheckSession(ctx context.Context, uid uint64, sid string) (isValid bool, err error) {
	if len(sid) <= 0 {
		isValid = false
		return
	}
	isExp, _, _, err := sessionDecode(uid, sid)
	if isExp {
		isValid = !isExp
		return
	}
	isValid, err = checkSessionFromRedis(ctx, uid, sid)
	return
}

func checkSessionFromRedis(ctx context.Context, uid uint64, sid string) (isValid bool, err error) {
	isValid = true
	key := getSessionKey(uid)
	redisStore := baseStore.GetRedisStore()
	result := redisStore.ZScore(ctx, key, sid)
	err = result.Err()
	res := result.Val()
	if err != nil || res <= 0 {
		isValid = false
	}
	return
}

func getSessionKey(uid uint64) string {
	key := KEY_SID_PRE + strconv.FormatUint(uid, 10)
	return key
}

func getTimeStr(ts uint64) string {
	if ts == 0 {
		return TIME_FMT
	}
	tm := time.Unix(int64(ts), 0)
	return tm.Format(TIME_FMT)
}

func sessionDecode(uid uint64, sid string) (bool, uint64, ClientType, error) {
	isExp := true
	expAt := uint64(0)
	clientType := CLIENT_APP

	defaulterr := errors.New("sid err")

	if sid[0:2] != SID_VERSION {
		return isExp, expAt, clientType, defaulterr
	}
	session := sid[2:]
	salt := session[4:12]
	body := session[0:4] + session[12:]
	dst, err := b64decode(body)
	if err != nil {
		return isExp, expAt, clientType, err
	}

	key := getRC4Key(salt)
	src, _ := rc4c(dst, key)

	l := strings.Split(src, "#")
	if len(l) < 4 {
		return isExp, expAt, clientType, defaulterr
	}

	targetUid, _ := strconv.ParseUint(l[0], 10, 64)
	if uid != targetUid {
		return isExp, expAt, clientType, defaulterr
	}
	expAt, err = strconv.ParseUint(l[2], 10, 64)
	if expAt > uint64(time.Now().Unix()) {
		isExp = false
	}
	if len(l) == 5 && l[4] == strconv.Itoa(int(CLIENT_WEB)) {
		clientType = CLIENT_WEB
	}

	return isExp, expAt, clientType, nil
}

func CreateSession(ctx context.Context, uid uint64, expAt uint64, cv string) (sid string, err error) {
	ctype := getClientType(cv)
	now := time.Now().Unix()
	if expAt < uint64(now) {
		expAt = uint64(now) + uint64(config.GlobalAppConfig.SessionConfig.Timeout/time.Second)
	}

	remainTime := expAt - uint64(now)

	salt := randString(16)
	head := salt[0:8]
	key := getRC4Key(head)
	src := fmt.Sprintf("%v#%v#%v#%v#%v", uid, salt[8:12], expAt, salt[12:], ctype)
	dst, _ := rc4c(src, key)
	preSid := b64encode(dst)
	sid = SID_VERSION + preSid[0:4] + head + preSid[4:]

	err = setSessionCache(ctx, uid, sid, time.Duration(remainTime)*time.Second)

	return
}

func setSessionCache(ctx context.Context, uid uint64, sid string, newRemainTime time.Duration) (err error) {
	redisStore := baseStore.GetRedisStore()
	key := getSessionKey(uid)
	score := time.Now().Unix()
	err = redisStore.ZAdd(ctx, key, &redis.Z{
		Score:  float64(score), // 成员的分值
		Member: sid,            // 成员的值
	}).Err()
	if err != nil {
		return
	}
	remainTime, err := redisStore.TTL(ctx, key).Result()
	if err != nil {
		return
	}

	if remainTime < newRemainTime {
		err = redisStore.Expire(ctx, key, newRemainTime).Err()
		if err != nil {
			return
		}
	}

	err = clearSession(ctx, uid)

	return

}

func clearSession(ctx context.Context, uid uint64) (err error) {
	redisStore := baseStore.GetRedisStore()
	key := getSessionKey(uid)
	result := redisStore.ZRange(ctx, key, 0, -1)
	err = result.Err()
	if err != nil {
		return
	}
	res := result.Val()
	mp := make(map[ClientType]string)
	removeList := []interface{}{}
	for i := range res {
		isExp, _, clientType, _ := sessionDecode(uid, res[i])
		if isExp {
			removeList = append(removeList, res[i])
			continue
		}
		olgValue, ex := mp[clientType]
		if ex {
			removeList = append(removeList, olgValue)
		}
		mp[clientType] = res[i]
	}

	if len(removeList) > 0 {
		err = redisStore.ZRem(ctx, key, removeList...).Err()
	}

	return
}

func getClientType(cv string) ClientType {
	array := strings.Split(cv, "_")
	if len(array) != 2 {
		return CLIENT_WEB
	}

	os := strings.ToLower(array[1])

	if strings.HasPrefix(os, "android") || strings.HasPrefix(os, "iphone") {
		return CLIENT_APP
	}

	return CLIENT_WEB
}

func randString(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = LETTERS[rand.Intn(len(LETTERS))]
	}
	return string(b)
}

func getRC4Key(salt string) string {
	return fmt.Sprintf("%x", md5.Sum([]byte(salt+RC4_PASS)))
}

func rc4c(src, key string) (string, error) {
	c, err := rc4.NewCipher([]byte(key))
	if err != nil {
		return "", err
	}
	dst := make([]byte, len(src))
	c.XORKeyStream(dst, []byte(src))
	return string(dst), nil
}

func b64encode(src string) string {
	s := base64.StdEncoding.EncodeToString([]byte(src))
	var dst string
	for i := 0; i < len(s); i++ {
		switch string(s[i]) {
		case "i":
			dst = dst + "i0"
		case "+":
			dst = dst + "i1"
		case "/":
			dst = dst + "i2"
		case "=":
			dst = dst + "i3"
		default:
			dst = dst + string(s[i])
		}
	}

	return dst
}

func b64decode(src string) (string, error) {
	var dst string
	for i := 0; i < len(src); i++ {
		if string(src[i]) == "i" {
			i++
			if i < len(src) {
				switch string(src[i]) {
				case "0":
					dst = dst + "i"
				case "1":
					dst = dst + "+"
				case "2":
					dst = dst + "/"
				case "3":
					dst = dst + "="
				default:
					dst = dst + "i" + string(src[i])
				}
			} else {
				dst = dst + "i"
			}

		} else {
			dst = dst + string(src[i])
		}
	}
	data, err := base64.StdEncoding.DecodeString(dst)
	if err != nil {
		return string(data), err
	}
	return string(data), nil
}
